/*
 * Imports files from the folders specified on the command line into iTunes.
 * Optionally deletes files that are referenced by iTunes but have been moved
 * or deleted.
 */

@cc_on

// iTunes track kinds.
@set @ITTrackKindUnknown     = 0; // Unknown track kind.  
@set @ITTrackKindFile      = 1; // File track (IITFileOrCDTrack).  
@set @ITTrackKindCD      = 2; // CD track (IITFileOrCDTrack).  
@set @ITTrackKindURL       = 3; // URL track (IITURLTrack).  
@set @ITTrackKindDevice    = 4; // Device track.  
@set @ITTrackKindSharedLibrary = 5; // Shared library track.  

// Valid file extensions for iTunes artwork.
var artwork_file_extensions = new ArrayDictionary({
  'jpg' : 'JPEG',
  'jpeg': 'JPEG',
  'gif' : 'GIF', 
  'png' : 'PNG', 
  'bmp' : 'Bitmap'});

// Valid file extensions for iTunes media.
var media_file_extensions = new ArrayDictionary({
  'mp3': 'MPEG-1 Audio Layer 3',
  'mp4': 'MPEG-4 Container',
  'aac': 'Advanced Audio Coding',
  'wav': 'WAVE Container',
  'aiff': 'Audio Interchange File Format'});

/*
 * Class that stores whether a directory should be added or removed from
 * iTunes and whether it should be recursively traversed.
 * 
 * Properties:
 *   name: Name of the directory.
 *   recurse: Whether to recursively traverse the file tree starting from this
 *   directory.
 *   add: Whether to add this directory to iTunes, a value of false assumes
 *   all files existing in this directory should be removed from iTunes.
 */
function DirectoryOption_toString() {
  return 'name=' + this.name + ' recurse=' + this.recurse.toString() +
    ' add=' + this.add.toString();
}

function DirectoryOption(name, recurse, add) {
  this.name  = name;
  this.recurse = recurse;
  this.add   = add;
}

DirectoryOption.prototype.toString = DirectoryOption_toString;

/*
 * Class that stores command line options.
 *
 * Properties:
 *   directories: Array of DirectoryOption arguments.
 *   sync: Whether to synchronize the contents of iTunes with the local file 
 *   system.
 *
 * Functions:
 *   parseArguments: Parse command line arguments storing the results in the 
 *   properties of this class.
 */
// Returns true if all options were parsed successfully, false otherwise.
function Options_parseArguments(arguments) {
  var number_of_arguments = arguments.length;
  for (var i = 0; i < number_of_arguments; i++) {
    if (arguments(i) == '-s') {
      this.sync = true;
    } else if (i + 1 >= number_of_arguments) {
      print('ERROR: Invalid argument ' + arguments(i).toString())
      return false;
    } else if (arguments(i) == '-d') {
      this.directories.push(
        new DirectoryOption(arguments(++i), false, true));
    } else if (arguments(i) == '-dr') {
      this.directories.push(
        new DirectoryOption(arguments(++i), true, true));
    } else if (arguments(i) == '-r') {
      this.directories.push(
        new DirectoryOption(arguments(++i), false, false));
    } else if (arguments(i) == '-rr') {
      this.directories.push(
        new DirectoryOption(arguments(++i), true, false));
    } else {
      print('ERROR: Invalid argument ' + arguments(i).toString());
      return false;
    }
  }
  return true;
}

function Options_toString() {
  var string = 'directories:\n';
  for (var i in this.directories) {
    string += '  ' + this.directories[i].toString() + '\n';
  }
  string += 'sync=' + this.sync.toString();
  return string;
}

function Options() {
  this.directories = new Array();
  this.sync    = false;
}

Options.prototype.parseArguments = Options_parseArguments;
Options.prototype.toString = Options_toString;

/*
 * Class that stores information about a media file (e.g mp3 etc.)
 * Properties:
 *   media: Path of the media file.
 *   add: Whether to add this file to iTunes, a value of false means this file
 *   should be removed from iTunes.
 *   artwork: Path of the artwork file associated with the media file.
 */
function MediaFile_toString() {
  return 'name=' + this.name + ' artwork=' + this.artwork + ' add=' +
    this.add.toString();
}

function MediaFile(name, add, artwork) {
  this.name = name;
  this.add = add;
  this.artwork = artwork;
}

MediaFile.prototype.toString = MediaFile_toString;

// Class which contains media files and artwork files.
function MediaAndArtworkFiles() {
  this.media_files_array = new Array();
  this.artwork_by_filename = new ArrayDictionary();
  this.artwork_by_directory = new DictionaryTree();
}

// List files in a directory.
function listDirectory(directory, recurse, file_array, lower_case) {
  recurse = defaultValue(recurse, false);
  file_array = defaultObject(file_array,  Array);
  lower_case = defaultValue(lower_case, true);
  var folder = file_system.GetFolder(directory);
  var file_it = new Enumerator(folder.files);
  for ( ; !file_it.atEnd(); file_it.moveNext()) {
    var path = file_it.item().Path;
    if (lower_case) {
      path = path.toLowerCase()
    }
    file_array.push(path);
  }
  if (recurse) {
    var subfolder_it = new Enumerator(folder.SubFolders);
    for ( ; !subfolder_it.atEnd(); subfolder_it.moveNext()) {
      listDirectory(subfolder_it.item().Path, recurse, file_array);
    }
  }
  return file_array;
}

/*
 * Traverse the specified array of OptionDirectory instances and create a
 * MediaAndArtworkFiles instance containing media and artwork files.
 */
function traverseOptionDirectories(option_directories) {
  var media_artwork_files = new MediaAndArtworkFiles();
  for (var i in option_directories) {
    var option_directory = option_directories[i];
    var directory_name = option_directory.name;
    var files = listDirectory(directory_name, option_directory.recurse);
    for (var j in files) {
      var file = files[j];
      var file_extension = file_system.GetExtensionName(file);
      if (media_file_extensions.exists(file_extension)) {
        // If this is a media file, store a reference to it.
        media_artwork_files.media_files_array.push(
          new MediaFile(file, option_directory.add));
      } else if (artwork_file_extensions.exists(file_extension)) {
        // Store the artwork filename.
        media_artwork_files.artwork_by_filename.add(file, file);
        media_artwork_files.artwork_by_directory.add(
          file_system.GetParentFolderName(file), file);
      }
    }
  }
  return media_artwork_files;
}

/*
 * Find artwork for each media file in a MediaAndArtwork instance.
 * This function searches for artwork by doing the following...
 * 1. Looking for any files that match the base of the media filename followed
 *  by one of the artwork_file_extensions.
 * 2. Looking for any files in the media file's directory that match one of 
 *  artwork_file_extensions.
 * 3. Searching any of the previously traversed parent directories of the
 *  directory containing the media file for artwork that meets either of the 
 *  previous criteria.
 *
 * The array of MediaFile instances is returned.
 */
function findArtworkForMediaFiles(media_artwork_files) {
  var media_files = media_artwork_files.media_files_array;
  var artwork_by_filename = media_artwork_files.artwork_by_filename;
  var artwork_by_directory = media_artwork_files.artwork_by_directory;
  var artwork_extensions = artwork_file_extensions.keys();
  for (var i in media_files) {
    var media_file = media_files[i];
    if (!isUndefined(media_file.artwork)) {
      continue;
    }

    var media_filename = media_file.name;
    var media_filepath = file_system.GetParentFolderName(media_filename);
    var artwork_basename = media_filepath + '\\' + 
        file_system.GetBaseName(media_filename);

    // Search for artwork with the file's name.
    for (var j in artwork_extensions) {
      var artwork_filename = artwork_basename + '.' + artwork_extensions[j];
      if (artwork_by_filename.exists(artwork_filename)) {
        media_file.artwork = artwork_filename;
        break;
      }
    }

    // If the artwork was found then move onto the next media file.
    if (!isUndefined(media_file.artwork)) {
      continue;
    }

    // Search directories for some artwork.
    while (media_filepath != '') {
      if (artwork_by_directory.exists(media_filepath)) {
        var artwork_files = artwork_by_directory.item(
          media_filepath).keyItems();
        for (var j in artwork_extensions) {
          var artwork_extension = artwork_extensions[j];
          for (var artwork_file in artwork_files) {
            if (artwork_extension == 
                file_system.GetExtensionName(artwork_file)) {
              media_file.artwork = artwork_file;
              break;
            }
          }
        }
        if (!isUndefined(media_file.artwork)) {
          break;
        }
      }
      media_filepath = file_system.GetParentFolderName(media_filepath);
    }
  }
  return media_files;
}

// Display the program usage.
function usage() {
  print('Usage: import_to_itunes.py working_directory [-s] [-d directory]');
  print('       [-rd directory] [-r directory] [-rr directory]');
  print('');
  print('working_directory:');
  print('    current working directory');
  print('-s:');
  print('    Sync iTunes with the current state of the file system.');
  print('    If files have been moved or deleted remove them from itunes');
  print('-d:');
  print('    Add a directory of files to iTunes.');
  print('-dr:');
  print('    Recursively add files within a directory to iTunes.');
  print('-r:');
  print('    Remove a directory of files from iTunes.');
  print('-rr:');
  print('    Recursively remove files within a directory from iTunes.');
  return -1;
}

function getiTunesTracksByFilename(tracks) {
  var tracks_by_filename = new Dictionary();
  var number_of_tracks = tracks.Count;
  for (var i = 1; i <= number_of_tracks; i++) {
    var track = tracks.Item(i);
    if (track.Kind == @ITTrackKindFile) {
      tracks_by_filename.add(track.Location, track);
    }
  }
  return tracks_by_filename;
}

function updateiTunesWithMediaFiles(media_files, library, tracks_by_filename) {
  for (var i in media_files) {
    var media_file = media_files[i];
    // iTunes sometimes crashes if files are added too quickly
    WScript.sleep(20);
    if (media_file.add) {
      if (tracks_by_filename.exists(media_file.name)) {
        continue;
      }

      var track_filename = file_system.GetBaseName(media_file.name)
      print('[add] ' + track_filename + ' || ' + 
          (isUndefined(media_file.artwork) ? 'no artwork' :
           media_file.artwork));

      var operation_status = library.AddFile(media_file.name);
      if (isUndefined(operation_status)) {
        print('error while trying to add file ' + track_filename);
      }
      while (operation_status.InProgress) {
      }

      if (!isUndefined(media_file.artwork)) {
        var tracks = operation_status.Tracks;
        var number_of_tracks = tracks.Count;
        for (var j = 1; j <= number_of_tracks; j++) {
          var track = tracks.Item(j);
          // Remove all artwork currently associated with the track.
          var artwork_collection = track.Artwork;
          while (artwork_collection.Count) {
            artwork_collection.Item(1).Delete();
          }
          // Add new artwork to the track.
          track.AddArtworkFromFile(media_file.artwork);
        }
      }
    } else {
      if (!tracks_by_filename.exists(media_file.name)) {
        continue;
      }
      print('[remove] ' + file_system.GetBaseName(media_file.name) +
          ' || ' + media_file.artwork);
      tracks_by_filename.item(media_file.name).Delete();
      tracks_by_filename.remove(media_file.name);
    }
  }
}

function main(arguments) {
  if (!arguments.length) {
    return usage();
  }

  var options = new Options();
  if (!options.parseArguments(arguments)) {
    return usage();
  }
  var media_files = findArtworkForMediaFiles(
    traverseOptionDirectories(options.directories));

  var i_tunes = new ActiveXObject('iTunes.Application');
  var library = i_tunes.LibraryPlaylist;
  updateiTunesWithMediaFiles(media_files, library,
                 getiTunesTracksByFilename(library.Tracks));
  return 0;
}

exit(main(WScript.Arguments));
